home *** CD-ROM | disk | FTP | other *** search
/ Personal Computer World 2009 February / PCWFEB09.iso / Software / Resources / Audio, Video & Photo / Songbird 0.7.0 / Songbird_0.7.0_windows-i686-msvc8.exe / components / sbMetadataImageScanner.js < prev    next >
Text File  |  2008-08-14  |  22KB  |  593 lines

  1. /**
  2. //
  3. // BEGIN SONGBIRD GPL
  4. // 
  5. // This file is part of the Songbird web player.
  6. //
  7. // Copyright(c) 2005-2008 POTI, Inc.
  8. // http://songbirdnest.com
  9. // 
  10. // This file may be licensed under the terms of of the
  11. // GNU General Public License Version 2 (the "GPL").
  12. // 
  13. // Software distributed under the License is distributed 
  14. // on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either 
  15. // express or implied. See the GPL for the specific language 
  16. // governing rights and limitations.
  17. //
  18. // You should have received a copy of the GPL along with this 
  19. // program. If not, go to http://www.gnu.org/licenses/gpl.html
  20. // or write to the Free Software Foundation, Inc., 
  21. // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  22. // 
  23. // END SONGBIRD GPL
  24. //
  25. */
  26.  
  27. // Variables for convience
  28. var Cc = Components.classes;
  29. var Ci = Components.interfaces;
  30. var Cu = Components.utils;
  31. var Cr = Components.results;
  32.  
  33. // Import helper modules
  34. Cu.import('resource://app/jsmodules/sbProperties.jsm');
  35. Cu.import("resource://app/jsmodules/sbLibraryUtils.jsm");
  36. Cu.import("resource://app/jsmodules/ArrayConverter.jsm");
  37. Cu.import('resource://gre/modules/XPCOMUtils.jsm');
  38.  
  39. /**
  40.  * Since we can't use the FUEL components until after all other components have
  41.  * been loaded we define a lazy getter here for when we need it.
  42.  */
  43. __defineGetter__("Application", function() {
  44.   delete this.Application;
  45.   return this.Application = Cc["@mozilla.org/fuel/application;1"]
  46.                               .getService(Ci.fuelIApplication);
  47. });
  48.  
  49. // Constanst for convinence
  50. const PROP_LAST_COVER_SCAN = SBProperties.base + 'lastCoverScan';
  51. const RESCAN_INTERVAL = (60*60*24*7*1000); // Rescan for missing art every week
  52. const TIMER_INTERVAL = (5 * 1000);         // perform a task every Xs
  53.  
  54. // Constants for topics to observe
  55. const SB_LIBRARY_MANAGER_READY_TOPIC = "songbird-library-manager-ready";
  56. const SB_LIBRARY_MANAGER_BEFORE_SHUTDOWN_TOPIC = "songbird-library-manager-before-shutdown";
  57.  
  58. // Constants for preferences
  59. const PREF_METADATA_IMAGESCANNER_DEBUG = "songbird.metadataimagescanner.debug";
  60. const PREF_METADATA_DEFAULT_COVER      = "songbird.metadataimagescanner.defaultCover";
  61. const PREF_METADATA_RESCAN_INTERVAL    = "songbird.metadataimagescanner.rescan";
  62. const PREF_METADATA_TIMER_INTERVAL     = "songbird.metadataimagescanner.interval";
  63.  
  64. /**
  65.  * sbMetadataImageScanner
  66.  * \brief A service that will scan through the main library searching the
  67.  *        metadata for a cover in each media item.
  68.  */
  69. function sbMetadataImageScanner () {
  70.   this._obsService = Cc["@mozilla.org/observer-service;1"]
  71.                        .getService(Ci.nsIObserverService);
  72.  
  73.   // We want to wait until the library is ready before starting up.
  74.   this._obsService.addObserver(this,
  75.                                SB_LIBRARY_MANAGER_READY_TOPIC,
  76.                                false);
  77.  
  78.   // We need to stop scanning before the library is shutdown.
  79.   this._obsService.addObserver(this,
  80.                                SB_LIBRARY_MANAGER_BEFORE_SHUTDOWN_TOPIC,
  81.                                false);
  82. };
  83. sbMetadataImageScanner.prototype = {
  84.   // Component Settings
  85.   className: "sbMetadataImageScanner",
  86.   constructor: sbMetadataImageScanner,       // Constructor to this object
  87.   classDescription: "Songbird Metadata Image Scanner",
  88.   classID: Components.ID("{59a57af8-1b1a-48ad-b81d-42afcf45d4f7}"),
  89.   contractID: "@songbirdnest.com/Songbird/Metadata/ImageScanner;1",
  90.   _xpcom_categories: [{ // Make this a service
  91.     category: "app-startup",
  92.     service: true
  93.   }],
  94.  
  95.   // Variables
  96.   DEBUG : false,            // debug flag
  97.   _timer: null,             // Timer for scanning the next item
  98.   _mediaListView: null,     // sbIMediaListView we are scanning
  99.   _itemViewIndex: 0,        // Index of next item in view we are to scan
  100.   _batch: null,             // Helper for batch items.
  101.   _mainLibrary: null,       // Hold it here since I have to QI it
  102.   _restartScanning: false,  // Set to true to restart the scanning
  103.   _rescanInterval: RESCAN_INTERVAL, // Time to pass for rescan of art
  104.   _timerInterval: TIMER_INTERVAL,   // Time to pass before next item scan
  105.   
  106.   // Services
  107.   _consoleService: null,    // For output of debug messages to error console
  108.   _obsService: null,         // For observer service to start/stop properly
  109.   _metadataManager: null,   // For getting metadata information from item
  110.   _ioService: null,         // For converting URIs and such
  111.  
  112.   /**
  113.    * Internal debugging functions
  114.    */
  115.   /**
  116.    * \brief Dumps out a message if the DEBUG flag is enabled with
  117.    *        the className pre-appended.
  118.    * \param message String to print out.
  119.    */
  120.   _debug: function (message)
  121.   {
  122.     if (!this.DEBUG) return;
  123.     try {
  124.       dump(this.className + ": " + message + "\n");
  125.       this._consoleService.logStringMessage(this.className + ": " + message);
  126.     } catch (err) {
  127.       // We do not want to throw an exception here
  128.     }
  129.   },
  130.   
  131.   /**
  132.    * \brief Dumps out an error message with the className + ": [ERROR]"
  133.    *        pre-appended, and will report the error so it will appear in the
  134.    *        error console.
  135.    * \param message String to print out.
  136.    */
  137.   _logError: function (message)
  138.   {
  139.     try {
  140.       dump(this.className + ": [ERROR] - " + message + "\n");
  141.       Cu.reportError(this.className + ": [ERROR] - " + message);
  142.     } catch (err) {
  143.       // We do not want to thow an exception here
  144.     }
  145.   },
  146.  
  147.   /*********************************
  148.    * Start of metadata specific functions
  149.    ********************************/
  150.   /**
  151.    * \brief Saves image data to a file.
  152.    * \param aImageData - Binary array of image data
  153.    * \param aImageDataSize - size of binary array
  154.    * \param aMimeType - mime type of image (image/png, image/jpg, etc)
  155.    * \return location of file as a fileURI spec
  156.    */
  157.   saveImageDataToFile: function(aImageData, aImageDataSize, aMimeType) {
  158.     // Generate a hash of the imageData for the filename
  159.     var cHash = Cc["@mozilla.org/security/hash;1"]
  160.                   .createInstance(Ci.nsICryptoHash);
  161.     cHash.init(Ci.nsICryptoHash.MD5);
  162.     cHash.update(aImageData, aImageDataSize);
  163.     var hash = cHash.finish(false);
  164.     var fileName = Array.prototype.map.call(hash,
  165.                                function charToHex(aChar) {
  166.                                 var charCode = aChar.charCodeAt(0);
  167.                                 return ("0" + charCode.toString(16)).slice(-2);
  168.                                }).join("");
  169.     
  170.     // grab the extension from the mimetype
  171.     var mimeService = Cc["@mozilla.org/mime;1"]
  172.                         .getService(Ci.nsIMIMEService);
  173.     var mimeInfo = mimeService.getFromTypeAndExtension(aMimeType, "");
  174.     if (!mimeInfo.getFileExtensions().hasMore()) {
  175.       this._debug("Unable to get extension for image data from [" +
  176.                   aMimeType + "]");
  177.       return null;
  178.     }
  179.     ext = mimeInfo.primaryExtension;
  180.     this._debug("Got extension of :" + ext);
  181.     
  182.     // Get the profile folder and append "artwork" as destination folder
  183.     var dir = Cc["@mozilla.org/file/directory_service;1"]
  184.                 .createInstance(Ci.nsIProperties);
  185.     var coverFile = dir.get("ProfLD", Ci.nsIFile);
  186.     coverFile.append("artwork");
  187.     // TODO: Check that we don't create other types of files like .exe
  188.     coverFile.append(fileName + "." + ext);
  189.   
  190.     // Make sure we have this file or are able to create it.
  191.     var outFilePath = this._ioService.newFileURI(coverFile);
  192.     if (coverFile.exists()) {
  193.       return outFilePath.spec;
  194.     }
  195.     
  196.     // The file does not exist so create it
  197.     try {
  198.       coverFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0655);
  199.     } catch (err) {
  200.       this._debug("Unable to create file: " + err);
  201.       return null;
  202.     }
  203.   
  204.     // Save data to the file
  205.     var outputFileStream =  Cc["@mozilla.org/network/file-output-stream;1"]
  206.                               .createInstance(Ci.nsIFileOutputStream);
  207.     outputFileStream.init(coverFile, -1, -1, 0);
  208.     // Need to write out as binary data
  209.     var binaryOutput = Cc["@mozilla.org/binaryoutputstream;1"]
  210.                          .createInstance(Ci.nsIBinaryOutputStream);
  211.     binaryOutput.setOutputStream(outputFileStream);
  212.     binaryOutput.writeByteArray(aImageData, aImageDataSize);
  213.     // Close the file so we are all clean.
  214.     outputFileStream.close();
  215.   
  216.     return outFilePath.spec;
  217.   },
  218.  
  219.   /**
  220.    * \brief Searches for an image in the metadata of a file and if it finds one
  221.    *        it saves it to a file, returning the file location.
  222.    * \param aMediaItem - Item to search for an image in
  223.    * \return location of the image. null for fail, "" for a file with no art.
  224.    */
  225.   fetchCoverForMediaItem: function (aMediaItem) {
  226.     // First check if this is a valid local file.
  227.     var fileURL = null;
  228.     var contentURL = aMediaItem.getProperty(SBProperties.contentURL);
  229.     try {
  230.       var contentURI = this._ioService.newURI(contentURL, null, null);
  231.       // if the URL isn't valid, fail
  232.       if (!contentURI) {
  233.         this._debug("Unable to find image for bad file: " + contentURL);
  234.         return null;
  235.       }
  236.  
  237.       // if the URL isn't local, fail
  238.       if ( !(contentURI instanceof Ci.nsIFileURL) ) {
  239.         this._debug("Unable to get image from non-local file: " +
  240.                        contentURL);
  241.         return null;
  242.       }
  243.     } catch (err) {
  244.       this._debug("Unable to find image for: " + contentURL + " - " + err);
  245.       return null;
  246.     }
  247.    
  248.     this._debug("Getting handler for mediaItem: " + contentURI.spec);
  249.     var handler = this._metadataManager.getHandlerForMediaURL(contentURI.spec);
  250.     if (!handler) {
  251.       this._debug("Unable to get handler for: " + contentURI.spec);
  252.       return null;
  253.     }
  254.  
  255.     try {
  256.       this._debug("Reading metadata from item [FRONTCOVER].");
  257.       var mimeTypeOutparam = {};
  258.       var outSize = {};
  259.       var imageData = handler.getImageData(Ci.sbIMetadataHandler
  260.                                              .METADATA_IMAGE_TYPE_FRONTCOVER,
  261.                                            mimeTypeOutparam,
  262.                                            outSize);
  263.       if (outSize.value <= 0) {
  264.         this._debug("Reading metadata from item [OTHER].");
  265.         // Try the OTHER cover
  266.         imageData = handler.getImageData(Ci.sbIMetadataHandler
  267.                                            .METADATA_IMAGE_TYPE_OTHER,
  268.                                          mimeTypeOutparam,
  269.                                          outSize);
  270.       }
  271.       
  272.       if (outSize.value > 0) {
  273.         this._debug("Found an image in the metadata.");
  274.         var outFileLocation = this.saveImageDataToFile(imageData,
  275.                                                        imageData.length,
  276.                                                        mimeTypeOutparam.value);
  277.         if (outFileLocation) {
  278.           this._debug("Saved data to file: " + outFileLocation);
  279.           return outFileLocation;
  280.         } else {
  281.           this._debug("Unable to save file: [" + outFileLocation + "]");
  282.         }
  283.       } else {
  284.         this._debug("Unable to find metadata: [" + contentURI.spec + "]");
  285.       }
  286.     } catch (err) {
  287.       this._debug("Unable to get image from metadata - for item at: " +
  288.                   contentURI.spec + " - " + err );
  289.     }
  290.     
  291.     return "";
  292.   },
  293.   
  294.   /**
  295.    * \brief Prepares to searche for an image in the metadata of a file and if it
  296.    *        finds one it saves it to a file and sets the primaryImageURL
  297.    *        property to that file.
  298.    * \param aMediaItem - Item to search for an image in
  299.    * \return True if an image was found, False if the item has no image.
  300.    */
  301.   _getCoverForItem: function (aMediaItem) {
  302.     // Check if there is no current cover
  303.     var primaryImageURL = aMediaItem.getProperty(SBProperties.primaryImageURL);
  304.     if (!primaryImageURL) {    
  305.  
  306.       // Check if we are ready to rescan this item
  307.       var lastCoverScan = parseInt(aMediaItem.getProperty(PROP_LAST_COVER_SCAN));
  308.       if (isNaN(lastCoverScan)) { lastCoverScan = 0; }
  309.       var timeNow = Date.now();
  310.       this._debug("Next item has lastCoverScan of [" + lastCoverScan + "] " +
  311.                    "Rescan Time = " + (lastCoverScan + this._rescanInterval) + " " +
  312.                   "Now = " + timeNow);
  313.       /**
  314.        * For now we only want to scan items once, so remove the rescan check
  315.        * and if we come to an item that has a lastCoverScan value we stop the
  316.        * scanner. The scanner will only restart when items have been added
  317.        * or removed. See Bug 11514
  318.        *
  319.       if ( (lastCoverScan + this._rescanInterval) < timeNow ) {
  320.        */
  321.       if (lastCoverScan <= 0) {
  322.         aMediaItem.setProperty(PROP_LAST_COVER_SCAN, timeNow);
  323.         var outFileLocation = this.fetchCoverForMediaItem(aMediaItem);
  324.         if (outFileLocation != null) {
  325.           // a successful scan of an empty file returns "",
  326.           // which will prevent us from scanning this file again as we
  327.           // set it into the media item here.
  328.           aMediaItem.setProperty(SBProperties.primaryImageURL,
  329.                                   outFileLocation);
  330.         } else {
  331.           // explicitly set it to "" so that other users can see that we
  332.           // have already checked the contents and explicitly noted that 
  333.           // there are none.
  334.           aMediaItem.setProperty(SBProperties.primaryImageURL, "");
  335.         }
  336.         return true;
  337.       } else {
  338.         // Since we are doing a second sort by lastCoverScan if we encounter
  339.         // one that is not ready to scan then we are done.
  340.         this._debug("Not ready to rescan this item for images.");
  341.         this._turnOffTimer();
  342.       }
  343.     } else {
  344.       // Since we are sorting by primaryImageURL (artwork) if we encounter an
  345.       // item with artwork then we are done.
  346.       this._debug("This item already has album art, this means we are done.");
  347.       this._turnOffTimer();
  348.     }
  349.     
  350.     return false;
  351.   },
  352.   /*********************************
  353.    * End of metadata specific functions
  354.    ********************************/
  355.  
  356.   /**
  357.    * \brief Grabs the next item from the view to scan.
  358.    */
  359.   _getNextItem: function () {
  360.     // Don't do anything if there is a batch running.
  361.     if (this._batch.isActive()) {
  362.       this._debug("Scanning is currently paused due to a batch running");
  363.       return;
  364.     }
  365.  
  366.     this._debug("Scanning for next item " +
  367.                  this._itemViewIndex + " of " +
  368.                  this._mediaListView.length);
  369.     
  370.     // if there are no items left to scan, then we are done.
  371.     if (this._itemViewIndex >= this._mediaListView.length) {
  372.       this._debug("All done searching");
  373.       this._turnOffTimer();
  374.       return;
  375.     }
  376.  
  377.     // Since we are sorting by the primaryImageURL (artwork) the items will be
  378.     // reordered every time an items pirmaryImageURL property changes. So here
  379.     // we take the next top item that we have not already scanned.
  380.     // We need a try/catch here because sometimes the next item is not ready,
  381.     // due to resort or something. We will just try the same item again on the
  382.     // next interval.
  383.     var nextItem = null;
  384.     try {
  385.       nextItem = this._mediaListView.getItemByIndex(this._itemViewIndex);
  386.     } catch (err) {
  387.       this._debug("Unable to get item, trying again at next interval:" + err);
  388.     }
  389.     
  390.     if (nextItem) {
  391.       this._debug("Getting next items cover");
  392.       if (!this._getCoverForItem(nextItem)) {
  393.         // Only increment the index if the item does not have an image
  394.         this._itemViewIndex++;
  395.       }
  396.     }
  397.   },
  398.  
  399.   /**
  400.    * \brief Starts a query for all items in the main library that do not have
  401.    *        the primaryImageURL set.
  402.    */
  403.   _getItems: function () {
  404.     this._mediaListView = this._mainLibrary.createView();
  405.     // Due to Bug 8489 we can not filter on null or empty values.
  406.     // Currently we are sorting by primaryImageURL so the empty ones are at
  407.     // The top.
  408.     var propArray =
  409.         Cc["@songbirdnest.com/Songbird/Properties/MutablePropertyArray;1"]
  410.           .createInstance(Ci.sbIMutablePropertyArray);
  411.     // Set strict to false so we do not need to validate PROP_LAST_COVER_SCAN
  412.     propArray.strict = false;
  413.     propArray.appendProperty(SBProperties.primaryImageURL, "a");
  414.     propArray.appendProperty(PROP_LAST_COVER_SCAN, "a");
  415.     this._mediaListView.setSort(propArray);
  416.  
  417.     // Remove the hidden or list items
  418.     var filter = LibraryUtils.createConstraint([
  419.       [
  420.         [SBProperties.isList, ["0"]]
  421.       ],
  422.       [
  423.         [SBProperties.hidden, ["0"]]
  424.       ]
  425.     ]);
  426.  
  427.     this._mediaListView.filterConstraint = filter;
  428.     this._debug("Scanning " + this._mediaListView.length + " items.");
  429.  
  430.     this._itemViewIndex = 0;
  431.     this._turnOnTimer();
  432.   },
  433.   
  434.   /**
  435.    * \brief Turns off the timer and cleans up related stuff
  436.    */
  437.   _turnOffTimer: function () {
  438.     if (this._timer) {
  439.       this._debug("Turning off timer");
  440.       this._timer.cancel();
  441.       this._timer = null;
  442.       this._mediaListView = null;
  443.       this._itemViewIndex = 0;
  444.     }
  445.   },
  446.   
  447.   /**
  448.    * \brief Turns on the timer, if the timer is currently on then it stops it
  449.    *        first
  450.    */
  451.   _turnOnTimer: function () {
  452.     if (this._timer) {
  453.       this._timer.cancel();
  454.     } else {
  455.       this._timer = Cc['@mozilla.org/timer;1'].createInstance(Ci.nsITimer);
  456.     }
  457.     this._timer.initWithCallback(this,
  458.                                  this._timerInterval, 
  459.                                  Ci.nsITimer.TYPE_REPEATING_SLACK);
  460.   },
  461.   
  462.   /**
  463.    * \brief Starts up the scanning of the main library for image in metadata.
  464.    */
  465.   _startup: function () {
  466.     // Check if we need debugging stuff
  467.     this.DEBUG = Application.prefs.getValue(PREF_METADATA_IMAGESCANNER_DEBUG,
  468.                                             false);
  469.     if (this.DEBUG) {
  470.       this._consoleService = Cc["@mozilla.org/consoleservice;1"]
  471.                               .getService(Ci.nsIConsoleService);
  472.     }
  473.     this._debug("Starting metadata image scanner");
  474.  
  475.     // Get some settings from the preferences
  476.     this._rescanInterval = Application.prefs.getValue(
  477.                                                   PREF_METADATA_RESCAN_INTERVAL,
  478.                                                   RESCAN_INTERVAL);
  479.     this._timerInterval = Application.prefs.getValue(
  480.                                                   PREF_METADATA_TIMER_INTERVAL,
  481.                                                   TIMER_INTERVAL);
  482.  
  483.     // Create our lastCoverScan property if it does not exist
  484.     var pMgr = Cc["@songbirdnest.com/Songbird/Properties/PropertyManager;1"]
  485.                 .getService(Ci.sbIPropertyManager);
  486.     if (!pMgr.hasProperty(PROP_LAST_COVER_SCAN)) {
  487.       var pI = Cc["@songbirdnest.com/Songbird/Properties/Info/Datetime;1"]
  488.                  .createInstance(Ci.sbIDatetimePropertyInfo);
  489.       pI.id = PROP_LAST_COVER_SCAN;
  490.       pI.displayName = "";
  491.       pI.userEditable = false;    // Hide this from the user completely.
  492.       pI.userViewable = false;
  493.       pI.remoteReadable = false;
  494.       pI.remoteWritable = false;
  495.       pMgr.addPropertyInfo(pI);
  496.     }
  497.  
  498.     // Load some services and such
  499.     this._metadataManager = Cc["@songbirdnest.com/Songbird/MetadataManager;1"]
  500.                               .getService(Ci.sbIMetadataManager);
  501.     this._ioService = Cc['@mozilla.org/network/io-service;1']
  502.                         .getService(Ci.nsIIOService);
  503.       
  504.     // Store this since we have to QI it.
  505.     this._mainLibrary = LibraryUtils.mainLibrary
  506.                           .QueryInterface(Ci.sbILocalDatabaseLibrary);
  507.  
  508.     try {
  509.       // Setup a listener to the main library for batches
  510.       this._batch = new LibraryUtils.BatchHelper();
  511.       this._mainLibrary.addListener(this,
  512.                                     true,
  513.                                     Ci.sbIMediaList.LISTENER_FLAGS_BATCHBEGIN |
  514.                                       Ci.sbIMediaList.LISTENER_FLAGS_BATCHEND,
  515.                                     null);
  516.   
  517.     } catch (err) {
  518.       this._logError("Unable to start scanner: " + err);
  519.       return;
  520.     }
  521.  
  522.     // Start the search
  523.     this._getItems();
  524.   },
  525.   
  526.   /**
  527.    * \brief Shutdown the scanning.
  528.    */
  529.   _shutdown: function () {
  530.     this._debug("Shutting down");
  531.     this._turnOffTimer();
  532.   },
  533.   
  534.   /*********************************
  535.    * nsITimerCallback
  536.    ********************************/
  537.   notify: function (aTimer) {
  538.     // Scan the next item for images
  539.     this._getNextItem();
  540.   },
  541.  
  542.   /*********************************
  543.    * sbIMediaListListener (requires nsISupportsWeakReference)
  544.    ********************************/
  545.   onBatchBegin: function (aMediaList) {
  546.     this._debug("Batch Begin Called");
  547.     this._batch.begin();
  548.     this._turnOffTimer();
  549.   },
  550.   
  551.   onBatchEnd: function onBatchEnd(aMediaList) {
  552.     this._debug("Batch End Called");
  553.     this._batch.end();
  554.     // If the batch has finished we need to restart the scanner in case any
  555.     // Items have been added or removed.
  556.     if (!this._batch.isActive()) {
  557.       this._debug("Batch is no longer running so restart scanning");
  558.       this._getItems();
  559.     }
  560.   },
  561.  
  562.   /*********************************
  563.    * nsIObserver
  564.    ********************************/
  565.   observe: function (aSubject, aTopic, aData) {
  566.     switch (aTopic) {
  567.       case SB_LIBRARY_MANAGER_READY_TOPIC:
  568.         this._obsService.removeObserver(this,
  569.                                        SB_LIBRARY_MANAGER_READY_TOPIC);
  570.         this._startup();
  571.         break;
  572.       case SB_LIBRARY_MANAGER_BEFORE_SHUTDOWN_TOPIC:
  573.         this._obsService.removeObserver(this,
  574.                                        SB_LIBRARY_MANAGER_BEFORE_SHUTDOWN_TOPIC);
  575.         this._shutdown();
  576.         break;
  577.     }
  578.   },
  579.  
  580.   /*********************************
  581.    * nsISupports
  582.    ********************************/
  583.   QueryInterface: XPCOMUtils.generateQI([Ci.sbIMetadataImageScanner,
  584.                                          Ci.sbIMediaListListener,
  585.                                          Ci.nsISupportsWeakReference,
  586.                                          Ci.nsITimerCallback])
  587. }
  588.  
  589.  
  590. function NSGetModule(compMgr, fileSpec) {
  591.   return XPCOMUtils.generateModule([sbMetadataImageScanner]);
  592. }
  593.